package com.chia.multienty.core.service.impl;

import cn.hutool.core.lang.Assert;
import com.alibaba.fastjson.JSONObject;
import com.aliyuncs.exceptions.ClientException;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.chia.multienty.core.cache.redis.service.api.HashRedisService;
import com.chia.multienty.core.cache.redis.service.api.StringRedisService;
import com.chia.multienty.core.domain.basic.Result;
import com.chia.multienty.core.domain.basic.WebLogUser;
import com.chia.multienty.core.domain.bo.PhoneCodeAuthentication;
import com.chia.multienty.core.domain.bo.workflow.ProcessStartResult;
import com.chia.multienty.core.domain.bo.workflow.WorkflowProcessNotifyBO;
import com.chia.multienty.core.domain.condition.UserOrOAuthCondition;
import com.chia.multienty.core.domain.constants.MultientyCacheKey;
import com.chia.multienty.core.domain.constants.MultientyConstants;
import com.chia.multienty.core.domain.dto.AppInstanceDTO;
import com.chia.multienty.core.domain.dto.PublicKeyDTO;
import com.chia.multienty.core.domain.dto.TenantDTO;
import com.chia.multienty.core.domain.enums.*;
import com.chia.multienty.core.domain.vo.DecryptedLoginInfo;
import com.chia.multienty.core.domain.vo.LoggedUserVO;
import com.chia.multienty.core.domain.vo.LoginResult;
import com.chia.multienty.core.dubbo.service.DubboWorkflowService;
import com.chia.multienty.core.exception.AuthenticationFailedException;
import com.chia.multienty.core.exception.KutaRuntimeException;
import com.chia.multienty.core.mapper.TenantMapper;
import com.chia.multienty.core.mybatis.KutaFuncEnum;
import com.chia.multienty.core.mybatis.MTLambdaWrapper;
import com.chia.multienty.core.mybatis.service.impl.KutaBaseServiceImpl;
import com.chia.multienty.core.parameter.tenant.*;
import com.chia.multienty.core.parameter.user.*;
import com.chia.multienty.core.parameter.workflow.FlowableProcessStartParameter;
import com.chia.multienty.core.parameter.workflow.WorkflowProcessDetailGetParameter;
import com.chia.multienty.core.pojo.*;
import com.chia.multienty.core.properties.yaml.YamlMultientyProperties;
import com.chia.multienty.core.rabbitmq.MultientyRabbitmqConstants;
import com.chia.multienty.core.scheduling.rabbitmq.MultientyWorkflowBoType;
import com.chia.multienty.core.service.*;
import com.chia.multienty.core.strategy.sms.SMSService;
import com.chia.multienty.core.strategy.sms.SMSServiceFactory;
import com.chia.multienty.core.strategy.sms.domain.SMSResult;
import com.chia.multienty.core.strategy.sms.domain.VerificationCodeBO;
import com.chia.multienty.core.tools.IdWorkerProvider;
import com.chia.multienty.core.tools.MultientyContext;
import com.chia.multienty.core.tools.TokenProvider;
import com.chia.multienty.core.util.*;
import com.github.yulichang.toolkit.MPJWrappers;
import com.google.common.collect.Lists;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.shardingsphere.transaction.annotation.ShardingSphereTransactionType;
import org.apache.shardingsphere.transaction.core.TransactionType;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Lazy;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import reactor.core.publisher.Mono;

import java.io.IOException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * <p>
 * 租户信息 服务实现类
 * </p>
 *
 * @author Multi Tenant Auto Generator
 * @since 2024-01-03
 */
@Service
@RequiredArgsConstructor
@Conditional(UserOrOAuthCondition.class)
@Slf4j
public class TenantServiceImpl extends KutaBaseServiceImpl<TenantMapper, Tenant>
        implements TenantService, MultientyUserService {

    @Autowired(required = false)
    private RedisTemplate redisTemplate;
    private final TokenProvider tokenProvider;
    private final HashRedisService hashRedisService;
    private final StringRedisService stringRedisService;
    private final YamlMultientyProperties multientyProperties;
    @Autowired(required = false)
    private DubboWorkflowService dubboWorkflowService;
    private final TenantRoleService tenantRoleService;
    private final PermissionService permissionService;
    private final AppInstanceService appInstanceService;
    @Autowired(required = false)
    private ProcessBindService processBindService;

    private final TenantSubAccountService tenantSubAccountService;

    private final ObjectProvider<Map<String,ReactiveAuthenticationManager>> authenticationManagerMap;

    @Autowired(required = false)
    @Lazy
    private DelegatingUserDetailsServiceImpl delegatingUserDetailsService;
    /**
     * 通过令牌获取租户信息
     * @param token 令牌
     * */
    @Override
    public TenantDTO getByToken(String token) {
        Assert.isTrue(token != null);

        TenantDTO tenant = hashRedisService.hgetAll(token, TenantDTO.class);
        return tenant.getTenantId() != null ? tenant : null;
    }

    @Override
    public void refreshToken(String token) {
        redisTemplate.expire(token, multientyProperties.getSecurity().getAuth().getRenewDuration(), TimeUnit.SECONDS);
    }

    /**
     * 保存令牌
     * @param token 令牌
     * */
    @Override
    public void saveToken(String token, Long tenantId) {
        AssertUtil.checkNull(token, "令牌");
        AssertUtil.checkNull(tenantId, "租户编号");
        stringRedisService.setJson(token, tenantId.toString(),
                multientyProperties.getSecurity().getAuth().getAccessTokenExpired());
    }

    @Override
    public PublicKeyDTO getPublicKey() {
        return PublicKeyDTO
                .builder()
                .publicKey(multientyProperties.getSecurity().getRsa().getPublicKey())
                .mockServer(false)
                .build();
    }

    @Override
    public String createToken(TenantDTO tenant) {
        String token = tokenProvider.createToken(tenant.getTenantId(), tenant.getCompanyName(),
                Arrays.asList(tenant.getRoleAlias().split(",")));
        return token;
    }

    @Override
    @SneakyThrows
    public SMSResult sendVerificationCode(LoginVerificationCodeSendParameter parameter) {
        TenantDTO tenant = getByPhone(parameter.getPhoneNumber(), Tenant::getTenantId);
        if(tenant == null || tenant.getTenantId() == null) {
            throw new KutaRuntimeException(HttpResultEnum.UN_REGISTERED_PHONE_NUMBER);
        }
        SMSService service = SMSServiceFactory.getService();
        if(service != null) {
            VerificationCodeBO bo = new VerificationCodeBO();
            String code = RandomStringUtils.getRandomCode(4, 0);
            stringRedisService.set(
                    String.format(MultientyConstants.TENANT_LOGIN_VERIFY_CODE_CACHE_KEY, parameter.getPhoneNumber()),
                    code,
                    MultientyConstants.VERIFY_CODE_CACHE_MILLS);
            bo.setCode(code);
            bo.setTargetPhoneNumbers(Lists.newArrayList(parameter.getPhoneNumber()));
            return service.sendVerificationCode(bo);
        }else {
            throw new KutaRuntimeException(HttpResultEnum.PATTERN_SERVICE_UNAVAILABLE, "SMS");
        }
    }

    /**
     * 检测登录失败超出阈值范围
     */
    private void checkLoginFailureExceedingThreshold(DecryptedLoginInfo info) {
        String cacheKey = String.format(MultientyConstants.LOGIN_FAILURE_COUNT_CACHE_KEY,
                info.getLoginMode().getCode(), info.getUsername());
        Object objCount = stringRedisService.get(cacheKey);
        if(objCount == null) {
            stringRedisService.set(cacheKey, String.valueOf(1), 30 * 60 * 1000);
            return;
        }
        int count = Integer.parseInt(objCount.toString());
        Integer threshold = multientyProperties.getLoginFailureThreshold();
        if(count + 1 >= threshold) {
            throw new AuthenticationFailedException(HttpResultEnum.EXCEEDING_LOGIN_FAILURE_THRESHOLD);
        }
        stringRedisService.set(cacheKey, String.valueOf(count + 1), 30 * 60 * 1000);
    }

    private void clearLoginFailureExceedingCache(LoginMode loginMode, String username) {
        String cacheKey = String.format(MultientyConstants.LOGIN_FAILURE_COUNT_CACHE_KEY,
                loginMode.getCode(), username);
        stringRedisService.delete(cacheKey);
    }

    /**
     * 登录的账户名和密码经过RAS公钥加密
     */
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public Mono<Result<LoginResult>> login(LoginParameter parameter) throws java.lang.Exception {
        String origStr = RsaUtil.decryptByPrivateKey(
                multientyProperties.getSecurity().getRsa().getPrivateKey(), parameter.getParam());
        DecryptedLoginInfo info = objectMapper.readValue(origStr, DecryptedLoginInfo.class);
        Mono<Authentication> authentication = null;
        checkLoginFailureExceedingThreshold(info);

        Authentication src = null;

        delegatingUserDetailsService.setApplicationType(ApplicationType.MERCHANT, info.getAccType());

        Optional<ReactiveAuthenticationManager> delegate = null;

        if(info.getLoginMode().equals(LoginMode.PHONE_CODE)) {
            delegate = ReactiveAuthenticationManagerUtil.getPhoneCodeAuthenticationManager(authenticationManagerMap);
        } else if(info.getLoginMode().equals(LoginMode.USERNAME_PASSWORD)) {
            delegate = ReactiveAuthenticationManagerUtil.getUsernamePasswordAuthenticationManager(authenticationManagerMap);
        }

        if(!delegate.isPresent()) {
            throw new KutaRuntimeException(500, "Please configure least one ReactiveAuthenticationManager.");
        }

        if(info.getLoginMode().equals(LoginMode.PHONE_CODE)) {
            src = new PhoneCodeAuthentication(info.getUsername(), info.getCode());
        } else if(info.getLoginMode().equals(LoginMode.USERNAME_PASSWORD)) {
            src = new UsernamePasswordAuthenticationToken(info.getUsername(), info.getPassword());
        }

        authentication = delegate.get().authenticate(src);

        return authentication.flatMap(auth-> {
            LoggedUserVO user = (LoggedUserVO) auth.getPrincipal();

            String token = tokenProvider.createToken(auth);
            parameter.setUserId(user.getLogUserId());
            WebLogUser.getInstance().setUserName(user.getName()).setUserId(user.getLogUserId());
            String cacheKey = String.format(MultientyCacheKey.PATTERN_LOGGED_TENANT, user.getLogUserId());
            stringRedisService.setJson(cacheKey, user, multientyProperties
                    .getSecurity()
                    .getAuth()
                    .getAccessTokenExpired() * 1000);
            clearLoginFailureExceedingCache(info.getLoginMode(), info.getUsername());
            return Mono.just(new Result<>(LoginResult.builder()
                    .code(200)
                    .msg("success")
                    .accessToken(token)
                    .build()));
        });
    }


    @Override
    public TenantDTO getByPhone(String phone, SFunction<Tenant, ?>... columns) {
        return baseMapper.selectJoinOne(TenantDTO.class,
                new MTLambdaWrapper<Tenant>()
                        .select(columns)
                        .eq(Tenant::getPhoneNumber, phone)
        );
    }

    @Override
    public void logout(LogoutParameter parameter) {
        Long userId = tokenProvider.getUserId(parameter.getToken());
        redisTemplate.delete(getCacheKey(userId));
    }


    @Override
    public TenantDTO getDetail(TenantDetailGetParameter parameter) {
        TenantDTO tenant = selectJoinOne(TenantDTO.class,
                MPJWrappers.<Tenant>lambdaJoin().eq(Tenant::getTenantId, parameter.getTenantId()));
        List<AppInstanceDTO> myAppInsList = appInstanceService.getMyAppInsList(parameter.getTenantId());
        tenant.setAppInsList(myAppInsList);
        return tenant;
    }

    @Override
    public IPage<TenantDTO> getPage(TenantPageGetParameter parameter) {
        return selectJoinListPage(parameter.getPageObj(), TenantDTO.class,
                new MTLambdaWrapper<Tenant>()
                        .solveGenericParameters(parameter)
                        .in(!ListUtil.isEmpty(parameter.getTenantIds()), Tenant::getTenantId, parameter.getTenantIds())
        );
    }

    @Override
    public void save(TenantSaveParameter parameter) {
        parameter.setPassword(MD5Util.md5WithSalt(parameter.getPassword()));
        Tenant tenant = new Tenant();
        BeanUtils.copyProperties(parameter, tenant);
        tenant.setTenantId(IdWorkerProvider.next());
        tenant.setTenantNo(RandomStringUtils.getRandomCode(6, 6));
        tenant.setExpired(false);
        tenant.setLocked(false);
        try {
            saveTE(tenant);
        }
        catch (Exception ex) {
            if(ex instanceof DuplicateKeyException) {
                throw new KutaRuntimeException(HttpResultEnum.DUPLICATE_USERNAME);
            }
        }
        parameter.setTenantId(tenant.getTenantId());
    }

    @Override
    public void update(TenantUpdateParameter parameter) {
        Tenant tenant = new Tenant();
        if(parameter.getPassword().length() < 20) {
            parameter.setPassword(MD5Util.md5WithSalt(parameter.getPassword()));
        }
        BeanUtils.copyProperties(parameter, tenant);
        updateByIdTE(tenant);
    }

    @Override
    public void delete(TenantDeleteParameter parameter) {
        removeByIdTE(parameter.getTenantId());
    }

    @Override
    public LoggedUserVO getLoggedInfo() throws KutaRuntimeException, IOException {
        return MultientyContext.getUser();
    }

    @Override
    public void updatePassword(PasswordUpdateParameter parameter) {
        String cacheKey = String.format(MultientyConstants.PASSWORD_UPDATE_CODE_CACHE_KEY,
                MultientyContext.getUser().getUserId());

        Object cachedCode = stringRedisService.get(cacheKey);
        if(cachedCode == null) {
            throw new KutaRuntimeException(HttpResultEnum.PASSWORD_UPDATE_CODE_EXPIRED);
        }
        if(!cachedCode.equals(parameter.getCode())) {
            throw new KutaRuntimeException(HttpResultEnum.VERIFICATION_CODE_ERROR);
        }
        Tenant tenant = getById(MultientyContext.getUser().getUserId());
        if(!MD5Util.verify(parameter.getOldPassword(), tenant.getPassword())) {
            throw new KutaRuntimeException(HttpResultEnum.OLD_PASSWORD_ERROR);
        }
        tenant.setPassword(MD5Util.md5WithSalt(parameter.getNewPassword()));
        updateById(tenant);
    }

    @Override
    public SMSResult sendRegisterVerCode(String phoneNumber) throws ClientException {
        log.info("发送商家注册验证码");
        SMSService service = SMSServiceFactory.getService();
        if(service != null) {
            VerificationCodeBO bo = new VerificationCodeBO();
            String code = RandomStringUtils.getRandomCode(4, 0);
            stringRedisService.set(
                    String.format(MultientyConstants.TENANT_LOGIN_VERIFY_CODE_CACHE_KEY, phoneNumber),
                    code,
                    MultientyConstants.VERIFY_CODE_CACHE_MILLS);
            bo.setCode(code);
            bo.setTargetPhoneNumbers(Lists.newArrayList(phoneNumber));
            return service.sendVerificationCode(bo);
        }else {
            throw new KutaRuntimeException(HttpResultEnum.PATTERN_SERVICE_UNAVAILABLE, "发送租户注册验证码");
        }
    }

    @Override
    @ShardingSphereTransactionType(TransactionType.BASE)
    @Transactional(rollbackFor = Exception.class)
    public void register(TenantRegisterParameter parameter) {
        log.info("注册新商家");
        String cacheKey = String.format(MultientyConstants.TENANT_LOGIN_VERIFY_CODE_CACHE_KEY, parameter.getPhoneNumber());
        Object cachedCode = stringRedisService.get(cacheKey);
        if(cachedCode == null) {
            throw new KutaRuntimeException(HttpResultEnum.VERIFICATION_CODE_EXPIRED);
        }
        if(!cachedCode.toString().equals(parameter.getCheckCode())) {
            throw new KutaRuntimeException(HttpResultEnum.VERIFICATION_CODE_ERROR);
        }
        parameter.setPassword(MD5Util.md5WithSalt(parameter.getPassword()));
        Tenant tenant = new Tenant();
        BeanUtils.copyProperties(parameter, tenant);
        tenant.setTenantId(IdWorkerProvider.next());
        tenant.setTenantNo(RandomStringUtils.getRandomCode(6, 6));
        tenant.setExpired(true);
        tenant.setLocked(false);
        tenant.setStatus(StatusEnum.REVIEWING.getCode());
        try {
            saveTE(tenant);
        }
        catch (Exception ex) {
            if(ex instanceof DuplicateKeyException) {
                throw new KutaRuntimeException(HttpResultEnum.DUPLICATE_USERNAME);
            }
        }

        TenantRole tenantRole = new TenantRole()
                .setRoleId(101L)
                .setTenantId(tenant.getTenantId());
        tenantRoleService.save(tenantRole);
        log.info("商家资料已保存,即将开启工作流");

        Map<String, Object> variables = new HashMap<>();
        variables.put(FlowableVariables.TENANT_ID.name(), tenant.getTenantId());
        variables.put(FlowableVariables.MQ_ROUTING_KEY.name(), MultientyRabbitmqConstants.USER_NOTIFY_IMME_ROUTING_KEY);
        variables.put(FlowableVariables.MQ_BO_TYPE.name(), MultientyWorkflowBoType.TENANT_APPROVED.name());
        variables.put(FlowableVariables.MQ_META_ID.name(), tenant.getTenantId());

        WorkflowProcess process = dubboWorkflowService.getWorkflowProcess(new WorkflowProcessDetailGetParameter()
                .setAppId(ApplicationType.PLATFORM.getValue().toString())
                .setTypeAlias("MERCHANT-REGISTER")
                .setKey("merchant_register")
        );
        List<ProcessBind> processBinds = processBindService.list(new LambdaQueryWrapper<ProcessBind>()
                .eq(ProcessBind::getProcessId, process.getProcessId())
                .in(ProcessBind::getMetaType, 1, 2));

        ProcessBind userBind = processBinds.stream().filter(p -> p.getMetaType().equals((byte) 1)).findAny().orElse(null);
        List<ProcessBind> groups = processBinds.stream().filter(p -> p.getMetaType().equals((byte) 2)).collect(Collectors.toList());
        if(userBind != null) {
            variables.put(userBind.getArgName(), userBind.getMetaId().toString());
        }
        if(groups.size() > 0) {
            Map<String, List<ProcessBind>> mappedGroup = groups.stream().collect(Collectors.groupingBy(ProcessBind::getArgName));
            mappedGroup.forEach((k,v) -> {
                variables.put(k, v.stream().map(m->m.getMetaId().toString()).collect(Collectors.toList()));
            });
        }
        if(log.isDebugEnabled()){
            log.debug("工作流参数:{}", JSONObject.toJSONString(variables));
        }
        ProcessStartResult processStartResult = dubboWorkflowService.start(new FlowableProcessStartParameter()
                .setKey("merchant_register")
                .setName("商户注册审批")
                .setAuthenticatedUserId(tenant.getTenantId().toString())
                .setVariables(variables)
        );
        log.info("工作流启动完毕,{}", JSONObject.toJSONString(processStartResult));
        parameter.setTenantId(tenant.getTenantId());
    }

    @Override
    public void handleTenantApprovedNotify(WorkflowProcessNotifyBO notify) {
        String status = notify.getVariables().get("status").toString();
        Long tenantId = Long.parseLong(notify.getVariables().get(FlowableVariables.TENANT_ID.name()).toString());
        Tenant tenant = getById(tenantId);
        switch (status) {
            case "APPROVED":
                tenant.setStatus(StatusEnum.NORMAL.getCode());
                tenant.setLocked(false);
                break;
            case "REJECT":
                tenant.setStatus(StatusEnum.DISABLED.getCode());
                tenant.setLocked(true);
                break;
        }
        updateById(tenant);
    }

    @Override
    public UserDetails findByUsername(String username) {
        TenantDTO tenant = selectJoinOne(
                TenantDTO.class,
                new MTLambdaWrapper<Tenant>()
                        .selectAll(Tenant.class)
                        .selectFunc(KutaFuncEnum.GROUP_CONCAT, Role::getName, TenantDTO::getRoleName)
                        .selectFunc(KutaFuncEnum.GROUP_CONCAT,Role::getAlias, TenantDTO::getRoleAlias)
                        .leftJoin(TenantRole.class, TenantRole::getTenantId,
                                Tenant::getTenantId)
                        .leftJoin(Role.class, Role::getRoleId,
                                TenantRole::getRoleId)
                        .and(and-> and.eq(Tenant::getUsername, username).or(or->or.eq(Tenant::getPhoneNumber, username)))
        );

        if(tenant == null) {
            throw new KutaRuntimeException(HttpResultEnum.USERNAME_OR_PASSWORD_ERROR);
        }

        if(tenant.getStatus().equals(StatusEnum.REVIEWING.getCode())) {
            throw new KutaRuntimeException(HttpResultEnum.TENANT_REVIEWING);
        }

        //监测当前租户应用实例状态
        AppInstanceDTO appIns = appInstanceService.getMyCurrentAppIns(tenant.getTenantId());
        if(appIns == null) {
            throw new KutaRuntimeException(HttpResultEnum.APP_INSTANCE_NOT_EXISTS);
        }

        List<Role> roles = tenantRoleService.getRoles(tenant.getTenantId());
        if(roles.size() > 0) {
            MultientyContext.setTenant(tenant);
            tenant.setPermissions(permissionService.getFuncPermissions(
                    new FuncPermissionListGetParameter()
                            .setOwner(ApplicationType.MERCHANT.getValue())
                            .setRoles(roles)
            ));
        }

        tenant.setSuperAdmin(true);
        tenant.setRoleIds(roles.stream().map(m->m.getRoleId()).collect(Collectors.toList()));
        if(tenant.getAvatar() == null) {
            tenant.setAvatar(MultientyConstants.DEFAULT_AVATAR);
        }
        LoggedUserVO loggedUser = new LoggedUserVO(tenant);
        loggedUser.setApplicationType(ApplicationType.MERCHANT);
        return loggedUser;
    }
}
